contents
Java Stream (스트림) 가이드
Java 8부터 도입된 Stream API는 컬렉션, 배열, 파일 등 연속된 데이터를 간결하고 선언형으로 처리할 수 있도록 해주는 기능입니다.
1. Stream이란?
- Stream은 데이터 구조가 아니라, 컬렉션·배열·I/O 등의 데이터 소스를 감싸 가공 및 처리하는 일련의 파이프라인입니다.
- Stream은 직접 데이터에 변경을 가하지 않으며, Java의 InputStream/FileStream과는 다른 개념입니다 (in-memory 처리).
2. Stream의 주요 특징
- 함수형 스타일: 어떻게(How)보다 무엇(What)을 처리할지에 초점
- 불변성: 원본 데이터를 수정하지 않고 새로운 결과 생성
- 지연 평가(Laziness): 최종 연산이 호출되기 전까지 실제 계산은 일어나지 않음
- 연속 처리: 여러 중간 연산을 메서드 체이닝으로 구성 가능
- 병렬 처리 가능:
.parallelStream()을 사용하면 멀티 코어 활용 가능
3. 스트림 파이프라인의 구조
- Source (소스): 데이터의 시작점 (리스트, 배열, 생성기 등)
- Intermediate Operation (중간 연산): 필터링/변환 등 변환을 위한 연산들 (예: map, filter, sorted)
- Terminal Operation (최종 연산): 스트림을 소비하는 연산 (예: collect, forEach, reduce)
List<String> result = names.stream()
.filter(s -> s.startsWith("A"))
.map(String::toUpperCase)
.sorted()
.collect(Collectors.toList()); // 결과 얻기 (리스트)
4. Stream 생성 방법
-
컬렉션에서 📦
List<String> list = Arrays.asList("foo", "bar"); list.stream(); -
배열에서 📦
Stream.of(1, 2, 3); Arrays.stream(new int[]{1, 2, 3}); -
값 직접 전달 📦
Stream.of("a", "b", "c"); -
생성 기반 📦
Stream.generate(Math::random).limit(5); Stream.iterate(0, n -> n + 2).limit(10); // 짝수 -
빌더 사용 📦
Stream.Builder<String> builder = Stream.builder(); builder.add("one").add("two"); Stream<String> s = builder.build();
5. 주요 스트림 연산
A. 중간 연산 (Intermediate operations)
| 연산 | 설명 |
|---|---|
| filter() | 조건에 맞는 요소만 필터링 |
| map() | 각 요소를 변환하거나 수정 |
| flatMap() | 중첩된 요소 펼치기 (List
|
| sorted() | 정렬 |
| distinct() | 중복 제거 |
| peek() | 중간 디버깅 또는 로깅 |
| limit(), skip() | 요소 수 제한 또는 스킵 |
B. 최종 연산 (Terminal operations)
| 연산 | 설명 |
|---|---|
| forEach() | 각 요소마다 수행 |
| collect() | 결과물 수집 (List, Set 등) |
| reduce() | 누적 집계 (합계, 곱 등) |
| count() | 요소 수 카운트 |
| anyMatch() | 하나라도 조건 만족하는지 검사 |
| allMatch() | 모두 조건 만족 여부 검사 |
| findFirst() | 첫 번째 요소 반환 (Optional) |
| min()/max() | Comparator 기준 최소/최대 찾기 |
6. 기본형 스트림 (IntStream, LongStream, DoubleStream)
IntStream.range(1, 10); // 1부터 9까지
LongStream.of(10L, 20L, 30L);
→ 박싱 비용 없이 효율적인 스트림 제공
7. 실전 예제 및 자주 쓰는 패턴
짝수 제곱값 리스트로 얻기
List<Integer> result = numbers.stream()
.filter(n -> n % 2 == 0)
.map(n -> n * n)
.collect(Collectors.toList());
중첩 리스트 펼치기
List<String> all = listOfLists.stream()
.flatMap(List::stream)
.collect(Collectors.toList());
그룹핑과 집계
Map<String, List<User>> byCity = users.stream()
.collect(Collectors.groupingBy(User::getCity));
8. Parallel Stream (병렬 처리)
List<Integer> bigList = ...;
bigList.parallelStream()
.filter(...)
.map(...)
.collect(Collectors.toList());
- 멀티코어 자동 병렬 분할
- 순서가 중요하지 않고 CPU 집약 작업일 때 효과적 (성능 테스트 필수)
9. for-loop vs Stream 비교
| 특징 | for-loop | Stream |
|---|---|---|
| 스타일 | 명령형 | 함수형, 선언형 |
| 병렬 처리 | 수동 구현 필요 | .parallelStream()으로 자동 병렬 처리 |
| 가독성 | 루프가 길어질수록 가독성 낮아짐 | 간결하고 체이닝 가능 |
| 성능 | 단순 반복에는 빠름 | 복잡한 변환·조합에 유리 |
10. 주의할 점
- Stream은 단 1번만 사용할 수 있습니다 (재사용 불가)
- 외부 상태 변경은 지양하고 사이드 이펙트 없는 코드 설계 권장
- null 값은 Stream에서 바로 사용할 수 없으므로 Optional 활용
- 고성능 목적이라면 단순 for 문이 유리할 때도 있음
11. 시각적 파이프라인 구성
[데이터 소스] → [filter] → [map] → [collect/forEach]
예시:
numbers.stream()
.filter(n -> n > 10)
.map(n -> n * 2)
.collect(Collectors.toList());
12. 실전 팁 & 모범 사례
- 반복적인 변환, 정렬, 필터링이 있으면 Stream 적극 활용
- 환경에 따라
.parallelStream()은 반드시 벤치마크 후 사용 - I/O 기반 스트림은 반드시 try-with-resources로 닫기
Collectors.groupingBy(),joining(),partitioningBy()등 풍부한 수집 도구 활용
✅ 결론 요약
- Java Stream은 함수형 스타일 프로그래밍을 가능하게 해주며, 데이터를 빠르게 필터링, 변환, 집계할 수 있는 강력한 도구입니다.
- 선언형, 불변성, 병렬성 등 현대적 프로그래밍 스타일을 적용하기 위한 핵심 API입니다.
- 단순 리스트 반복뿐 아니라, 큰 데이터 처리 파이프라인, 리포트 생성, 분산 처리 등 다양한 영역에서 활용됩니다.
✅ Java Stream 주요 사용 예제
Java의 Stream API를 활용한 가장 일반적인 코드 예제를 보여드리겠습니다.
1. 📌 필터링, 변환(map), 결과 수집(collect)
짝수만 필터링하고 제곱한 후 리스트로 수집
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6);
List<Integer> evenSquares = numbers.stream()
.filter(n -> n % 2 == 0) // 짝수만 통과
.map(n -> n * n) // 제곱값 변환
.collect(Collectors.toList()); // 결과를 리스트로 저장
// 결과: [4, 16, 36]
2. 🏙️ 그룹핑 및 카운트
사용자를 도시별로 그룹화하고 수를 센다
class User { String city; String name; /* getter 생략 */ }
List<User> users = List.of(
new User("Seoul", "Kim"),
new User("Busan", "Lee"),
new User("Seoul", "Park")
);
Map<String, Long> usersPerCity = users.stream()
.collect(Collectors.groupingBy(
User::getCity, // 도시 기준 그룹화
Collectors.counting() // 그룹당 개수 카운트
));
// 결과: {Seoul=2, Busan=1}
3. ➕ 합계 및 곱 (Reduce)
모든 숫자의 합 또는 곱
List<Integer> numbers = Arrays.asList(1, 2, 3, 4);
int sum = numbers.stream().mapToInt(Integer::intValue).sum(); // 합계: 10
int product = numbers.stream().reduce(1, (a, b) -> a * b); // 곱: 24
4. 🪆 중첩 리스트 평탄화 (flatMap)
List<List<String>> → List<String>
List<List<String>> listOfLists = List.of(
List.of("a", "b"),
List.of("c", "d")
);
List<String> flat = listOfLists.stream()
.flatMap(Collection::stream)
.collect(Collectors.toList()); // 결과: [a, b, c, d]
5. 🥇 최대/최소 찾기 (max/min)
가장 점수가 높은 사용자 찾기
class User { String name; int score; /* getter 생략 */ }
Optional<User> topUser = users.stream()
.max(Comparator.comparing(User::getScore));
topUser.ifPresent(u -> System.out.println(u.getName() + " is top!"));
6. ✅ 조건 검사 - allMatch / anyMatch / noneMatch
List<Integer> values = Arrays.asList(3, 4, 5);
boolean allAboveTwo = values.stream().allMatch(v -> v > 2); // 모두 2 이상? → true
boolean anyAboveFour = values.stream().anyMatch(v -> v > 4); // 하나 이상 4 초과? → true
boolean noneNegative = values.stream().noneMatch(v -> v < 0); // 음수 없음? → true
7. 🔁 중복 제거 및 정렬
List<Integer> raw = Arrays.asList(5, 3, 5, 4, 3, 2);
List<Integer> uniqSorted = raw.stream()
.distinct() // 중복 제거
.sorted() // 오름차순 정렬
.collect(Collectors.toList()); // 결과: [2, 3, 4, 5]
8. 📖 페이징 처리 (Limit & Skip)
List<String> names = Arrays.asList("Kim", "Lee", "Park", "Choi", "Jung", "Yoon");
// 페이지 2 (3건씩 자를 때, 두 번째 페이지)
List<String> page2 = names.stream()
.skip(3) // 앞의 3개 건너뜀
.limit(3) // 다음 3개 선택
.collect(Collectors.toList()); // 결과: [Choi, Jung, Yoon]
9. 🔗 문자열 합치기 (joining)
List<String> words = Arrays.asList("spring", "java", "stream");
String result = words.stream()
.collect(Collectors.joining(", ")); // "spring, java, stream"
10. ⚡ 병렬 스트림 (Parallel Stream)
List<Double> bigList = ...;
bigList.parallelStream()
.map(Math::sqrt)
.forEach(System.out::println);
- CPU를 잘 활용하는 연산에 적합 (예: 숫자 연산, 통계, 정제 등)
- 결과 순서 보장이 필요 없다면 퍼포먼스 개선 가능
이 예제들은 스트림을 활용한 Java의 주요 작업 유형들을 포괄합니다:
⭐ 필터링, 매핑, 집계, 정렬, 그룹화, 페이징, 병렬 처리 등
🔧 Java Stream 성능 최적화 – 상세 설명 및 실전 예제
Java의 Stream API는 코드를 간결하고 함수형 스타일로 작성할 수 있게 해주지만, 항상 가장 빠른 방법은 아닙니다. 특히 큰 데이터나 복잡한 스트림 체이닝에서는 성능에 주의해야 합니다. 여기에는 실무에서 꼭 알아야 할 성능 튜닝 기법과 코드 예제를 정리했습니다.
1. 전통 루프 vs Stream 성능
- 단순한 연산에는 기존 for문이 Stream보다 훨씬 빠를 수 있습니다.
- Stream은 복합 연산, 파이프라인 처리, 병렬 처리에 적합합니다.
// 일반 for문 – 빠름
int sum = 0;
for (int n : numbers) sum += n;
// Stream 사용 – 간단한 데이터셋에선 오히려 느릴 수 있음
int streamSum = numbers.stream().mapToInt(Integer::intValue).sum();
2. Primitive Stream (IntStream 등) 사용하기
박싱/언박싱 오버헤드 제거
// boxing 발생 (성능 손실)
int sum = numbers.stream().reduce(0, Integer::sum);
// 비박싱 사용 – 속도 향상
int sum2 = numbers.stream()
.mapToInt(Integer::intValue)
.sum();
3. 불필요한 객체 생성 피하기
스트림 내에서 매핑 시 새로운 객체를 생성하면 GC 부담 증가
// ❌ 매번 새 Wrapper 객체 생성
stream.map(val -> new Wrapper(val)).collect(...);
// ✅ 필요 없다면 identity 사용
stream.map(Function.identity()).collect(...);
4. parallelStream()은 신중히 사용할 것
.parallelStream()은 CPU 연산이 크고 상태가 없는 작업에서 효과적- IO 기반, 순서 의존, 적은 요소에서는 오히려 느릴 수 있음
double sum = bigList.parallelStream()
.mapToDouble(Math::sqrt)
.sum();
✔ 반드시 성능 측정 후 사용 (벤치마크, 프로파일링으로 확인)
5. 비싼 연산은 스트림 밖에서
// ❌ 매번 스트림 안에서 계산
stream.map(n -> expensive(n)).collect(...);
// ✅ 외부에서 먼저 계산 후 스트림 활용
double res = expensiveCalculation();
stream.map(n -> n * res).collect(...);
6. groupingBy 같은 Collector 활용하기
복잡한 수작업 루프 트랜지션 보다는 Collectors API 활용이 더 효율적
// ✔ 한 번의 패스로 도시별 그룹핑
Map<String, List<User>> byCity = users.stream()
.collect(Collectors.groupingBy(User::getCity));
7. 단축 연산 사용 (short-circuiting)
anyMatch, findFirst, limit 등은 조건이 충족되면 앞당겨 종료
// 첫 매칭 발견 시 즉시 종료 (전체 순회 안 함)
boolean found = users.stream()
.anyMatch(u -> u.getEmail().endsWith("@gmail.com"));
8. 부작용(Side effects) 피하기
forEach나map내에서 외부 상태 변경 시 멀티스레드 안전 문제 발생 가능- 병렬 스트림에서 특히 위험
9. 순서 무시할 수 있다면 unordered() 활용
Set<Integer> set = numbers.stream()
.unordered()
.collect(Collectors.toSet());
- 순서 보장이 필요 없다면
.unordered()를 통해 병렬 처리 효율을 높일 수 있음
10. 실 데이터 기반 벤치마크 필수
System.nanoTime()으로 측정하거나, JMH, VisualVM 등 프로파일러 사용- Stream과 for-loops의 성능은 다음에 따라 달라짐:
- 데이터 크기
- primitive vs boxed
- 중간 연산 수, 복잡도
- 순서 보장 여부
✔ 요약 테이블
| 팁 | 이유 | 예시 |
|---|---|---|
| IntStream 사용 | 박싱/언박싱 제거 | IntStream.range(1, n) |
| ParallelStream은 고부하에서만 | 스레드 오버헤드 있음 | .parallelStream().map(...) |
| 부작용 피하기 | 안정성 및 병렬 처리 문제 방지 | .forEach()에서 외부 리스트 건드리지 않기 |
| short-circuit 연산 사용 | 빠른 종료 가능 | anyMatch(...), limit(...) |
| 그룹핑 시 groupingBy 사용 | 성능 + 가독성 동시 확보 | Collectors.groupingBy(...) |
✔ 실전 예제: 고속 스트림 패턴
💡 수백만 개의 숫자 중에서 짝수 중 가장 큰 100개를 제곱하여 합산
int result = numbers.stream()
.parallel()
.filter(n -> n % 2 == 0)
.distinct()
.sorted(Comparator.reverseOrder())
.limit(100)
.mapToInt(n -> n * n)
.sum();
- 병렬 스트림 사용
- 정렬 최적화 (
limit을 미리 쓰면 속도 향상) mapToInt로 unboxing 제거
✅ 결론 요약
- Stream은 품질 높은 코드에는 좋지만, 성능을 얻기 위해선 신중한 설계 필요
- 단순 루프보다 느릴 수 있다는 점도 고려해야 함
primitive,parallel,short-circuit,collectors를 잘 조합하면 고성능 작성 가능
references